這裡是「Three.js學習日誌」的第10篇,本篇的主旨是要介紹紋理與材質的關係,這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。
其實我們在前幾天的內容有大概給大家展示過紋理(Texture)的使用,但沒有講得很Detail,所以今天我特別規劃了一篇文章要用來講解這個部分。
在3D建模的知識圈中,通常會有紋理(Texture)和材質(Material)這兩個專有名詞交互穿插於文獻之中。
通常紋理(Texture)這個詞指的是:
一組圖像數據,用來表示3D模型表面上的細節。
而材質(Material)這個詞則指的是:
一組定義光照模型如何與表面交互的係數。例如決定物體表面的反射率/漫反射率,...etc.
儘管概念不相似,但這兩者其實常常被混為一談,尤其是有些人已經習慣把紋理(Texture)稱作材質貼圖,導致兩者之間的界線更加模糊。
在Three.js中,若想要導入紋理,例如我想要給一個方塊表面具有磚塊的圖樣,那就會需要用到TextureLoader。
通常我們會在專案裡面建立一個TextureLoader的實例。然後反覆地用TextureLoader.load這個方法去讀入材質。
const tl = new TextureLoader();
const brickTexture = tl.load('../img/brick.png');
TextureLoader.load這個方法其實有點像是jquery的$.ajax,他也一樣可以傳入載入目標的url,還有onLoad/onError時的callback;
有些人可能會覺得好奇為什麼TextureLoader.load這邊不設計成回傳Promise。
畢竟大家都愛Promise,Promise讚!。
但其實官方有說因為這樣改下去會牽涉到大幅度改動,所以暫時沒有規劃。
對這個話題有興趣的人可以看這篇文章
如果想要讓紋理載入可以採Promise機制,這邊會需要自己做包裝。
import { TextureLoader } from "https://cdn.skypack.dev/three";
const tl = new TextureLoader();
const getTexture = (url) => {
return new Promise((res, rej) => {
tl.load(
url,
(texture) => {
res(texture);
},
null //因為TextureLoader目前不支援OnProgress,但是卻又留了一個空的參數欄位,所以必須給null
,
rej
);
});
};
async function main() {
const someTexture = await getTexture("https://picsum.photos/id/237/200/300");
console.log(someTexture);
//接著就可以在這邊取用someTexture
}
main();
接著我們順便介紹另外一個Class,他的名字叫做LoadingManager。
我們可以把LoadingManager的實例傳進去TextureLoader.constructor裡面。
而每當被植入這個LoadingManager實例的Loader系函數進入onLoad/onProgress/OnError階段的時候,LoadingManager就會通報我們階段的發生。
這個功能通常適用在網頁剛載入,但資源還沒有完全載入,因此需要顯示一個載入條UI的狀況。
我們把上面的Promise包裝範例稍作改造,用來示範如何使用LoadingManager
import { TextureLoader, LoadingManager } from "https://cdn.skypack.dev/three";
const lm = new LoadingManager();
lm.onStart = (url, itemsLoaded, itemsTotal) => {
//onStart會在每一項資源開始載入的時候被執行
//可以選擇用console.log的方式去顯示url, itemsLoaded, itemsTotal的狀況
//url是該項資源的位址
//itemsLoaded是目前一共已經有多少資源完成載入
//itemsTotal是當前所有需要載入的資源數量
console.log(
"開始載入: " +
url +
".\nLoaded " +
itemsLoaded +
" of " +
itemsTotal +
" files."
);
};
lm.onProgress = (url, itemsLoaded, itemsTotal) => {
console.log( '已載入: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
lm.onError = (url) => {
//onError會在每一項資源載入失敗的時候被執行
};
const tl = new TextureLoader(lm);
const getTexture = (url) => {
return new Promise((res, rej) => {
tl.load(
url,
(texture) => {
res(texture);
},
null,
rej
);
});
};
//以上部分其實可以包裝成es6 module,這樣用起來就更優雅了。
async function main() {
const someTexture1 = await getTexture("https://picsum.photos/id/237/200/300");
const someTexture2 = await getTexture("https://picsum.photos/id/238/200/300");
}
main();
codepen連結:點我

透過TextureLoader拿到紋理之後,要怎麼使用呢?
其實我們前幾天已經有示範過了,這邊就再讓我示範一次~
最簡單的方式其實就是把紋理填入Material的map屬性。
假設我們要用的Material是MeshStandardMaterial
async function main() {
const someTexture1 = await getTexture("https://picsum.photos/id/237/200/300");
const mat = new MeshStandardMaterial({
map:someTexture1
})
}
就這麼簡單。
常常有種狀況就是,當我們把紋理貼到模型上面的時候,才發現紋理貼圖的位置好像對不上模型。 又或者是有時候我們會需要去Repeat一個紋理,就像css的background-repeat一樣。
這種時候就需要調整Texture本身的屬性。
平移可以透過Texture.offset這個屬性來完成。
Texture.offset的型別會是Vector2。
texture.offset.x = 0.5;
texture.offset.y = 0.5;
旋轉的話就是Texture.rotation。他的型別是number,所以我們這邊必須要帶入Radian的數值。
預設情況下他是以材質貼圖的左下角做為旋轉中心,我們可以透過改變Texture.center這個Vector2的X/Y值來改變旋轉中心。
texture.rotation = 0.5 * Math.PI;
我們之前有講解過uv是什麼,但是卻沒有解釋uv這個名稱的由來。
uv其實代表的是材質貼圖的X軸(又稱U軸),和Y軸(又稱V軸),之所以這樣命名是因為要避免跟3D物體的XY軸混淆。
這邊如果我們想要讓材質沿著U軸重複,首先我們必須要先把Texture.wrapS設置為RepeatWrapping,而如果是要沿著V軸重複材質,則是要先把Texture.wrapT設置為RepeatWrapping。
這邊要注意
RepeatWrapping是一個常數,必須要從Three.js的module中引入。
設置完wrapS/wrapT之後,接著就是設置Texture.repeat,他也一樣是一個Vector2。
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
texture.repeat.x = 2;
texture.repeat.y = 3;
其實Three.js並沒有提供原生的材質貼圖縮放功能。
但我們可以透過調整Texture.repeat的值來實現這件事。
這邊記得不要去設定
Texture.wrapS/Texture.wrapT,除非你除了放大縮小,還想要有重複
texture.repeat.x = 0.5; //小於1的值會造成放大
texture.repeat.y = 2; //大於1的值會造成縮小
通常我們在實作大量的紋理重複時,我們會碰到一個狀況。這邊我用圖片展示給大家看一下。

這張圖的左右兩邊圖樣都是透過大量重複同樣的紋理才形成的。
而當我們把攝影機往下移動。

可以發現右半邊的圖形,好像看起來有種不自然的抖動感。
其實這是一種GPU成像的特性,有興趣的人可以看看這篇文
而如果想要消除這種不自然的感覺,讓右邊的圖變得跟左邊一樣,那就需要使用Mipmapping。
在Three.js中,與Mipmapping相關的設置是Texture.minFilter。在預設之下,Texture.minFilter的值是開放Mipmapping的,但如果碰到不需要使用Mipmapping的材質(例如材質沒有像上圖一樣有大量重複的紋樣),我們可以把它換成別的。
例如:
texture.minFilter = LinearFilter; //注意LinearFilter也是個需要引入的常數
另外,因為在Three.js中,Mipmapping的原理是透過預先渲染出一系列低畫素模糊版本的材質貼圖,才達成的。
就像這樣。

所以如果真的要關閉Mipmapping,我們還需要停止預渲染上圖材質的動作。
texture.generateMipmaps = false;
今天我們講解了Texture的一些基本操作,但這還只是學習Material的第一步而已。
接著我預期至少也還要2天以上的時間才可以結束掉這個章節。
還請大家繼續追蹤~